/*
 * Copyright 2022 The Open Islands Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openislands.oi.manager.union;


import org.openislands.oi.bootstrap.Bootstrap;
import org.openislands.oi.constant.MessageCodeEnum;
import org.openislands.oi.error.LogicException;
import org.openislands.oi.pojo.base.Generic;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/***
 * Used to handle multi-table association operations
 */
@Service
public class UnionPipelineManager implements ApplicationListener<ContextRefreshedEvent> {
    /***
     * Default get targetClass from {@link ApplicationContext#getBean}
     * If you want to customize it, you need to add to Cache in {@link #onApplicationEvent}
     */
    private final Map<Class<?>, Object> ordinaryClassCache = new HashMap<>();

    /***
     * UnionPipelineHandler Cache
     * Key see {@link Generic}
     * Value see Beans implements {@link UnionPipelineHandler}
     * Load see {@link #onApplicationEvent}
     */
    private final Map<Generic<?>, UnionPipelineHandler<?>> unionPipelineHandlerCache = new HashMap<>();

    @Transactional(timeout = 300)
    public <Params> void fire(UnionPipeline<Params> unionPipeline) {
        fire(unionPipeline.getHeadContext());
    }

    /**
     * Get UnionPipelineHandler from UnionPipelineHandler Cache and fire UnionPipeline
     *
     * @throws LogicException if get handler is null
     */
    @Transactional(timeout = 300)
    @SuppressWarnings("unchecked")
    public <Params> void fire(Generic<Params> key, Params params) throws LogicException {
        UnionPipelineHandler<Params> unionPipelineHandler = (UnionPipelineHandler<Params>) unionPipelineHandlerCache.get(key);
        LogicException.nonNull(unionPipelineHandler, MessageCodeEnum.DATA_NOT_FOUND_ERROR, key + ": union pipeline handler is null");
        UnionPipeline<Params> unionPipeline = UnionPipeline.create(params);
        unionPipelineHandler.handle(params, unionPipeline);
        fire(unionPipeline.getHeadContext());
    }

    /***
     * params = context.prev.nextParams
     * result = context.handle(target, params)
     * if (exception)
     *    context.rollbackHandler.handle(target, params)
     *    threw exception
     * fire(context.next)
     */
    private <Target, Params, Result> void fire(UnionHandlerContext<Target, Params, Result> handlerContext) {
        // get target bean
        Class<Target> targetClass = handlerContext.getTargetClass();
        Target target = ordinaryClass(targetClass);
        if (target == null) {
            ApplicationContext applicationContext = Bootstrap.applicationContext();
            target = applicationContext.getBean(targetClass);
        }

        // get current context params
        Params params = handlerContext.getParams();
        try {
            // handle context, set next params
            Result nextNodeParams = handlerContext.getHandler().handle(target, params);
            handlerContext.setNextParams(nextNodeParams);
            // invoke next context
            UnionHandlerContext<?, Result, ?> nextNode = handlerContext.getNext();
            if (nextNode != null) {
                fire(nextNode);
            }
        } catch (Exception e) {
            // roll back needed. throw exception to prev context
            if (!handlerContext.canRollback()) {
                return;
            }
            // if the rollback handler is not null, handle rollback
            UnionHandler<Target, Params, Result> rollbackHandler = handlerContext.getRollbackHandler();
            if (Objects.nonNull(rollbackHandler)) {
                rollbackHandler.handle(target, params);
            }
            throw e;
        }
    }

    @SuppressWarnings("unchecked")
    public <Target> Target ordinaryClass(Class<Target> targetClass) {
        Object obj = ordinaryClassCache.get(targetClass);
        return obj != null ? (Target) obj : null;
    }

    public <Target> void addOrdinaryClass(Class<Target> targetClass, Target target) {
        ordinaryClassCache.put(targetClass, target);
    }

    /**
     * Load {@link #ordinaryClassCache} and {@link #unionPipelineHandlerCache}
     */
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        this.addOrdinaryClass(UnionPipeline.EmptyDefine.class, new UnionPipeline.EmptyDefine());

        ApplicationContext applicationContext = event.getApplicationContext();
        Map<String, UnionPipelineRegisterEvent> beansOfType = applicationContext.getBeansOfType(UnionPipelineRegisterEvent.class);
        UnionPipelineRegister unionPipelineRegister = unionPipelineHandlerCache::put;
        for (UnionPipelineRegisterEvent registerEvent : beansOfType.values()) {
            registerEvent.unionPipelineRegisterEvent(unionPipelineRegister);
        }
    }
}
